# boston_tf.py
import numpy as np
import pandas as pd
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import os

# 支持中文
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
tf.random.set_seed(42)
np.random.seed(42)

def download_boston_data():
    """下载波士顿房价数据集并保存到本地"""
    import pandas as pd
    import os

    # 检查数据文件是否已存在
    if not os.path.exists('boston_housing.csv'):
        print("正在下载波士顿房价数据集...")

        try:
            url = 'http://lib.stat.cmu.edu/datasets/boston'

            # 读取原始数据（所有行）
            raw_data = pd.read_csv(url, sep='\s+', skiprows=21, header=None)

            # 重新组织数据格式 - 原始数据中每两行对应一个完整的样本
            data = []
            for i in range(0, len(raw_data), 2):
                # 获取两行的数据并合并
                row_part1 = raw_data.iloc[i].dropna().values
                row_part2 = raw_data.iloc[i + 1].dropna().values
                combined_row = np.concatenate([row_part1, row_part2])
                data.append(combined_row)

            # 定义列名
            column_names = [
                'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS',
                'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'PRICE'  # 注意这里用PRICE而不是MEDV
            ]

            # 创建DataFrame
            df = pd.DataFrame(data, columns=column_names)

            # 保存到本地
            df.to_csv('boston_housing.csv', index=False)
            print("数据已保存到 boston_housing.csv")

        except Exception as e:
            print(f"下载数据失败: {e}")
            print("尝试从sklearn加载数据...")
            try:
                from sklearn.datasets import load_boston
                boston = load_boston()
                X = boston.data
                y = boston.target

                feature_names = boston.feature_names
                df = pd.DataFrame(X, columns=feature_names)
                df['PRICE'] = y
                df.to_csv('boston_housing.csv', index=False)
                print("数据已从sklearn加载并保存到 boston_housing.csv")
            except:
                print("sklearn加载也失败，请检查网络或安装")
                return None
    else:
        print("使用本地已存在的数据文件 boston_housing.csv")

    # 读取数据
    df = pd.read_csv('boston_housing.csv')
    return df

# 1. 下载或加载数据
df = download_boston_data()
print(df.head())

X = np.hstack([df.values[::2, :], df.values[1::2, :2]])  # 506×13
y = df.values[1::2, 2]                                   # 506

# 2. 标准化 & 拆分
X = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)

# 3. 构建回归模型
model = tf.keras.Sequential([
    tf.keras.layers.Dense(64, activation='relu', input_shape=(13,)),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(32, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(1)  # 线性输出
])

model.compile(optimizer='adam', loss='mse', metrics=['mae'])

# 4. 训练
history = model.fit(X_train, y_train,
                    validation_split=0.2,
                    epochs=200,
                    batch_size=16,
                    verbose=0)

# 5. 评估
test_mse, test_mae = model.evaluate(X_test, y_test, verbose=0)
print(f"\n测试集 MAE: {test_mae:.3f} 万$/套")

# 6. 绘制并单独保存训练曲线
# 第一个图：平均绝对误差
plt.figure(figsize=(8, 6))
plt.plot(history.history['mae'], label='训练 MAE')
plt.plot(history.history['val_mae'], label='验证 MAE')
plt.title('平均绝对误差')
plt.xlabel('Epoch')
plt.ylabel('MAE')
plt.legend()
plt.tight_layout()
plt.savefig('mae_curve.png')  # 保存第一个图
plt.show()

# 第二个图：均方误差
plt.figure(figsize=(8, 6))
plt.plot(history.history['loss'], label='训练 MSE')
plt.plot(history.history['val_loss'], label='验证 MSE')
plt.title('均方误差')
plt.xlabel('Epoch')
plt.ylabel('MSE')
plt.legend()
plt.tight_layout()
plt.savefig('mse_curve.png')  # 保存第二个图
plt.show()

# 7. 预测示范
sample = X_test[:5]
pred = model.predict(sample, verbose=0).flatten()
true = y_test[:5]
print("\n预测示范（单位：万$/套）:")
for p, t in zip(pred, true):
    error = abs(p - t)  # 计算绝对误差
    print(f"  预测={p:6.2f}  真实={t:6.2f}  误差={error:.2f}")